1 /**
  2  * Constructs a new, empty panel with default properties. Panels, with the
  3  * exception of the root panel, are not typically constructed directly; instead,
  4  * they are added to an existing panel or mark via {@link pv.Mark#add}.
  5  *
  6  * @class Represents a container mark. Panels allow repeated or nested
  7  * structures, commonly used in small multiple displays where a small
  8  * visualization is tiled to facilitate comparison across one or more
  9  * dimensions. Other types of visualizations may benefit from repeated and
 10  * possibly overlapping structure as well, such as stacked area charts. Panels
 11  * can also offset the position of marks to provide padding from surrounding
 12  * content.
 13  *
 14  * <p>All Protovis displays have at least one panel; this is the root panel to
 15  * which marks are rendered. The box model properties (four margins, width and
 16  * height) are used to offset the positions of contained marks. The data
 17  * property determines the panel count: a panel is generated once per associated
 18  * datum. When nested panels are used, property functions can declare additional
 19  * arguments to access the data associated with enclosing panels.
 20  *
 21  * <p>Panels can be rendered inline, facilitating the creation of sparklines.
 22  * This allows designers to reuse browser layout features, such as text flow and
 23  * tables; designers can also overlay HTML elements such as rich text and
 24  * images.
 25  *
 26  * <p>All panels have a <tt>children</tt> array (possibly empty) containing the
 27  * child marks in the order they were added. Panels also have a <tt>root</tt>
 28  * field which points to the root (outermost) panel; the root panel's root field
 29  * points to itself.
 30  *
 31  * <p>See also the <a href="../../api/">Protovis guide</a>.
 32  *
 33  * @extends pv.Bar
 34  */
 35 pv.Panel = function() {
 36   pv.Bar.call(this);
 37 
 38   /**
 39    * The child marks; zero or more {@link pv.Mark}s in the order they were
 40    * added.
 41    *
 42    * @see #add
 43    * @type pv.Mark[]
 44    */
 45   this.children = [];
 46   this.root = this;
 47 
 48   /**
 49    * The internal $dom field is set by the Protovis loader; see lang/init.js. It
 50    * refers to the script element that contains the Protovis specification, so
 51    * that the panel knows where in the DOM to insert the generated SVG element.
 52    *
 53    * @private
 54    */
 55   this.$dom = pv.Panel.$dom;
 56 };
 57 
 58 pv.Panel.prototype = pv.extend(pv.Bar)
 59     .property("canvas")
 60     .property("overflow");
 61 
 62 pv.Panel.prototype.type = "panel";
 63 
 64 /**
 65  * The canvas element; either the string ID of the canvas element in the current
 66  * document, or a reference to the canvas element itself. If null, a canvas
 67  * element will be created and inserted into the document at the location of the
 68  * script element containing the current Protovis specification. This property
 69  * only applies to root panels and is ignored on nested panels.
 70  *
 71  * <p>Note: the "canvas" element here refers to a <tt>div</tt> (or other suitable
 72  * HTML container element), <i>not</i> a <tt>canvas</tt> element. The name of
 73  * this property is a historical anachronism from the first implementation that
 74  * used HTML 5 canvas, rather than SVG.
 75  *
 76  * @type string
 77  * @name pv.Panel.prototype.canvas
 78  */
 79 
 80 /**
 81  * Default properties for panels. By default, the margins are zero, the fill
 82  * style is transparent.
 83  *
 84  * @type pv.Panel
 85  */
 86 pv.Panel.prototype.defaults = new pv.Panel()
 87     .extend(pv.Bar.prototype.defaults)
 88     .fillStyle(null)
 89     .overflow("visible");
 90 
 91 /**
 92  * Returns an anchor with the specified name. This method is overridden since
 93  * the behavior of Panel anchors is slightly different from normal anchors:
 94  * adding to an anchor adds to the anchor target's, rather than the anchor
 95  * target's parent. To avoid double margins, we override the anchor's proto so
 96  * that the margins are zero.
 97  *
 98  * @param {string} name the anchor name; either a string or a property function.
 99  * @returns {pv.Anchor} the new anchor.
100  */
101 pv.Panel.prototype.anchor = function(name) {
102 
103   /* A "view" of this panel whose margins appear to be zero. */
104   function z() { return 0; }
105   z.prototype = this;
106   z.prototype.left = z.prototype.right = z.prototype.top = z.prototype.bottom = z;
107 
108   var anchor = pv.Bar.prototype.anchor.call(new z(), name)
109       .data(function(d) { return [d]; });
110   anchor.parent = this;
111   return anchor;
112 };
113 
114 /**
115  * Adds a new mark of the specified type to this panel. Unlike the normal
116  * {@link Mark#add} behavior, adding a mark to a panel does not cause the mark
117  * to inherit from the panel. Since the contained marks are offset by the panel
118  * margins already, inheriting properties is generally undesirable; of course,
119  * it is always possible to change this behavior by calling {@link Mark#extend}
120  * explicitly.
121  *
122  * @param {function} type the type of the new mark to add.
123  * @returns {pv.Mark} the new mark.
124  */
125 pv.Panel.prototype.add = function(type) {
126   var child = new type();
127   child.parent = this;
128   child.root = this.root;
129   child.childIndex = this.children.length;
130   this.children.push(child);
131   return child;
132 };
133 
134 /** @private TODO */
135 pv.Panel.prototype.bind = function() {
136   pv.Mark.prototype.bind.call(this);
137   for (var i = 0; i < this.children.length; i++) {
138     this.children[i].bind();
139   }
140 };
141 
142 /**
143  * @private Evaluates all of the properties for this panel for the specified
144  * instance <tt>s</tt> in the scene graph, including recursively building the
145  * scene graph for child marks.
146  *
147  * @param s a node in the scene graph; the instance of the panel to build.
148  * @see Mark#scene
149  */
150 pv.Panel.prototype.buildInstance = function(s) {
151   pv.Bar.prototype.buildInstance.call(this, s);
152   if (!s.children) s.children = [];
153 
154   /*
155    * Build each child, passing in the parent (this panel) scene graph node. The
156    * child mark's scene is initialized from the corresponding entry in the
157    * existing scene graph, such that properties from the previous build can be
158    * reused; this is largely to facilitate the recycling of SVG elements.
159    */
160   for (var i = 0; i < this.children.length; i++) {
161     this.children[i].scene = s.children[i]; // possibly undefined
162     this.children[i].build();
163   }
164 
165   /*
166    * Once the child marks have been built, the new scene graph nodes are removed
167    * from the child marks and placed into the scene graph. The nodes cannot
168    * remain on the child nodes because this panel (or a parent panel) may be
169    * instantiated multiple times!
170    */
171   for (var i = 0; i < this.children.length; i++) {
172     s.children[i] = this.children[i].scene;
173     delete this.children[i].scene;
174   }
175 
176   /* Delete any expired child scenes, should child marks have been removed. */
177   s.children.length = this.children.length;
178 };
179 
180 /**
181  * @private Computes the implied properties for this panel for the specified
182  * instance <tt>s</tt> in the scene graph. Panels have two implied
183  * properties:<ul>
184  *
185  * <li>The <tt>canvas</tt> property references the DOM element, typically a DIV,
186  * that contains the SVG element that is used to display the visualization. This
187  * property may be specified as a string, referring to the unique ID of the
188  * element in the DOM. The string is converted to a reference to the DOM
189  * element. The width and height of the SVG element is inferred from this DOM
190  * element. If no canvas property is specified, a new SVG element is created and
191  * inserted into the document, using the panel dimensions; see
192  * {@link #createCanvas}.
193  *
194  * <li>The <tt>children</tt> array, while not a property per se, contains the
195  * scene graph for each child mark. This array is initialized to be empty, and
196  * is populated above in {@link #buildInstance}.
197  *
198  * </ul>The current implementation creates the SVG element, if necessary, during
199  * the build phase; in the future, it may be preferrable to move this to the
200  * update phase, although then the canvas property would be undefined. In
201  * addition, DOM inspection is necessary to define the implied width and height
202  * properties that may be inferred from the DOM.
203  *
204  * @param s a node in the scene graph; the instance of the panel to build.
205  */
206 pv.Panel.prototype.buildImplied = function(s) {
207   if (!this.parent) {
208     var c = s.canvas;
209     if (c) {
210       if (typeof c == "string") c = document.getElementById(c);
211 
212       /* Clear the container if it's not associated with this panel. */
213       if (c.$panel != this) {
214         c.$panel = this;
215         c.innerHTML = "";
216       }
217 
218       /* If width and height weren't specified, inspect the container. */
219       var w, h;
220       if (s.width == null) {
221         w = parseFloat(pv.css(c, "width"));
222         s.width = w - s.left - s.right;
223       }
224       if (s.height == null) {
225         h = parseFloat(pv.css(c, "height"));
226         s.height = h - s.top - s.bottom;
227       }
228     } else if (s.$canvas) {
229 
230       /*
231        * If the canvas property is null, and we previously created a canvas for
232        * this scene node, reuse the previous canvas rather than creating a new
233        * one.
234        */
235       c = s.$canvas;
236     } else {
237 
238       /**
239        * Returns the last element in the current document's body. The canvas
240        * element is appended to this last element if another DOM element has not
241        * already been specified via the <tt>$dom</tt> field.
242        */
243       function lastElement() {
244         var node = document.body;
245         while (node.lastChild && node.lastChild.tagName) {
246           node = node.lastChild;
247         }
248         return (node == document.body) ? node : node.parentNode;
249       }
250 
251       /* Insert a new container into the DOM. */
252       c = s.$canvas = document.createElement("span");
253       this.$dom // script element for text/javascript+protovis
254           ? this.$dom.parentNode.insertBefore(c, this.$dom)
255           : lastElement().appendChild(c);
256     }
257     s.canvas = c;
258   }
259   pv.Bar.prototype.buildImplied.call(this, s);
260 };
261